SQL Injection
アプリケーションに想定外のSQLを実行させる攻撃。
例
以下はそれぞれのサーバーごとにユーザーの簡単なメモを保存するbotです。
code:bot.js
const Database = require('better-sqlite3');
const { Client, MessageEmbed } = require("discord.js");
const db = new Database('sqli.db', { verbose: console.log });
const client = new Client({
allowedMentions: {
parse: []
}
});
const prefix = "!";
const token = process.env.DISCORD_TOKEN;
db.exec("CREATE TABLE IF NOT EXISTS memos(guild integer,id integer,value text,PRIMARY KEY(id,guild))");
async function onMessage(message) {
if (!message.guild) {
return;
}
const l1, ...lines = message.content.split("\n");
if (l1 === prefix + "set") {
const value = lines.join("\n").trim();
if (value.length == 0 ){
//よくないけどDiscordの気が狂わない限り大丈夫
db.exec(DELETE FROM memos WHERE guild = ${message.guild.id} AND id = ${message.author.id});
await message.channel.send("メモを削除しました。");
return;
}
//よくない
db.exec(INSERT OR REPLACE INTO memos VALUES (${message.guild.id},${message.author.id},'${value}'));
await message.channel.send("メモを設定しました。");
return;
}
const cmd, ...args = l1.split(" ");
if (cmd === prefix + "get") {
//よくない
const memo = db.prepare(SELECT value FROM memos WHERE guild = ${message.guild.id} AND id = ${args[0] ?? message.author.id}).get();
if (memo) {
await message.channel.send(memo.value);
} else {
await message.channel.send(new MessageEmbed().setDescription("該当のユーザーのメモは見つかりませんでした。"));
}
}
}
client.on("message", (message) => {
onMessage(message).catch((err) => console.error(err));
});
client.login(token);
このbotに対して次のようなリクエストを投げます。(クライアントからタブ文字を送る方法がわからなかったのでfetchを使っています)
code:request.js
fetch("https://discord.com/api/v8/channels/<Channel Id>/messages", {
"headers": {
"accept": "*/*",
"accept-language": "en-GB",
"authorization": "<TOKEN>",
"content-type": "application/json",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin"
},
"body": "{\"content\":\"!get 0\\tOR\\tTRUE\\tORDER\\tBY\\tguild,id\\tLIMIT\\t1\\tOFFSET\\t1\"}",
"method": "POST",
"mode": "cors"
});
OFFSETを変化させていくことで、それぞれのサーバーにおけるすべてのユーザーのメモを取得することができます。
https://gyazo.com/ec07fed3e04c6d05b64ca1fde013b0b2
また、以下のように、あるユーザーのメモを置き換えるような攻撃も可能です。
code:message.txt
!set
'),(494780225280802817,735835853372522546,' @tigはバカだ') --
https://gyazo.com/9ba65d007df805ea0d299ff943f0c71b
対策
Prepared Statementを使用することで大抵のSQL Injectionに対処可能です。
code:sqli.js
async function onMessage(message) {
const l1, ...lines = message.content.split("\n");
if (!message.guild) {
return;
}
if (l1 === prefix + "set") {
const value = lines.join("\n").trim();
if (value.length == 0) {
//改善した
db.prepare(DELETE FROM memos WHERE guild = ? AND id = ?).run(message.guild.id, message.author.id);
await message.channel.send("メモを削除しました。");
return;
}
//改善した
db.prepare(INSERT OR REPLACE INTO memos VALUES (?,?,?)).run(message.guild.id, message.author.id, value);
await message.channel.send("メモを設定しました。");
return;
}
const cmd, ...args = l1.split(" ");
if (cmd === prefix + "get") {
//改善した
const memo = db.prepare(SELECT value FROM memos WHERE guild = ? AND id = ?).get(message.guild.id, args0 ?? message.author.id);
if (memo) {
await message.channel.send(memo.value);
} else {
await message.channel.send(new MessageEmbed().setDescription("該当のユーザーのメモは見つかりませんでした。"));
}
}
}
https://gyazo.com/eb69f92fb9ed2975d1df74c46586576d
どうにもならないこともあるのでその時はバリデーションを行ってください。
code: command.txt
!search name=x
code: search.js
function search(searchText) {
return db.prepare(SELECT name,exp FROM users WHERE ${searchText.split(" ").join(" AND ")}).all();
}
これは次のように修正できます。
code: search.js
const allowedColumn = new Set("id","name");
function search(searchText) {
const kv = searchText.split(" ")
.map(e => e.split("="))
.filter((k, v) => allowedColumn.has(k));
if (kv.length === 0) {
throw new Error("invalid searchText");
}
return db.prepare(SELECT id,name,exp FROM users WHERE ${kv.map(([k, v]) => k + "=?").join(" AND ")} LIMIT 100).all(kv.map((k, v) => v));
}